非同步 (asynchronous) 與串流 (stream),是程式開發時經常必須面對的議題,各自有各自解決的問題,也各自都有其帶來的延伸問題,今天就先來對這兩個東西有基礎的理解吧!
在 JavaScript 中有個 setTimeout
方法,會在指定的時間(毫秒) 後執行裡面的程式碼邏輯,請問以下程式有兩個 console.log
會分別印出 A 和 B,請問印出的順序為?
setTimeout(() => {
console.log('A');
}, 1000);
console.log('B');
相信使用過 setTimeout()
的人都知道答案是 2,而其中的原因就是「非同步」。
到底什麼是非同步呢?我們先來說明一下「同步」是什麼?
先想下一下以下程式印出的順序為何?
console.log('A');
console.log('B');
相信所有人都會毫不猶豫地回答「先印出 A 再印出 B」,因為我們通常在閱讀程式碼時候是按照前後順序閱讀的,自然而然會認為程式碼是按照前後順序執行的,也就是先發生的程式碼先處理,而這個按照前後發生順序執行的行為我們就稱為「同步」行為。
因此,不一定按照這種順序的行為我們就稱為「非同步」行為。那麼為什麼要有「非同步」這種行為?全部照順序執行不是很好嗎?我們可以想像一下 setTimeout(() => console.log('A'), 1000)
這樣的程式碼,如果一定「等待」一秒鐘,在畫面操作上會發生什麼事情?
最簡單的理解就是在這一秒鐘畫面因為全心等待要去執行 console.log('A')
而導致其他互動完全無法處理,一秒鐘聽起來還好,但如果換成是 AJAX 呼叫後端 API 的請求,偏偏伺服器處理又要等待比較長的時間,數十秒甚至數分鐘都有可能的時候呢?我們真的能接受畫面完全卡住那麼長的一段時間卻什麼都不做嗎?
相信大多數人的答案應該都是「不能」。
這種情況下「非同步」的設計就變得非常重要,當程式並沒有任何其他的運算,只是單純的「等待」時,我們就把它丟到一個暫存區內,讓畫面可以繼續處理其他的行為,直到等待到我們要的資料時,在拿回處理的所有權,繼續後續的程式運算。
當然這只是很粗淺的說明非同步存在的必要性跟流程,它的背後原理有很多東西可以說,但我們的目標是學習 RxJS,因此在這裡只需要知道基本的觀念就好,關於非同步處理的深入原理,網路上有非常多深度介紹的文章,有興趣可以自行搜尋一下。
透過非同步我們可以避免等待時間畫面卡住的時間浪費,也就是俗稱的「non blocking I/O」,然而明顯的缺點是這樣的處理方式把程式邏輯變得更複雜了,畢竟這跟多數人閱讀程式碼的習慣不同,但只要理解什麼時候應該是非同步處理,其實習慣後也不會造成太大困擾,且 JavaScript 也提供了 Promise 這個好用的非同步處理 API 來簡化一些複雜的問題。而另一個問題是,一般的非同步就是「等待完成」後就結束了,若有一系列的等待和先後順序等行為,就需要搭配到串流了。
相信大家都有在網路上看過線上影片的經驗,如果是高畫質影片,通常都要數十 MB 甚至數 GB 以上,如果每個次都要把整個影片檔案下載完才能播放,那麼體驗之差相信難以想像。
但假設我們將影片依照時間切成一小段一小段,只需要數秒鐘就能載入完成的影片片段,當播放器快要播放到某個時間點時,再去下載這個時間點對應的片段呢?是不是就不用等那麼長的時間啦!
這就是串流背後做的事情,將資料分成小小的片段,再「串」起來分段「流」向同一個地方,以上述的例子來說就是播放影片的邏輯。
以播放影片的例子來說,大概會寫出像這樣的程式碼:
// 建立一個 stream 物件
const videoPlayStream = {
// 用來存放每個時間片段的影片內容
videoObject: [],
downloadVideo: minute => {
// 如果影片片段已存在,直接回傳
if (videoPlayStream.videoObject[minute]) {
return Promise.resolve(videoPlayStream.videoObject[minute]);
} else {
// 如果影片片段不存在,則下載
return fetch(`...?minute=${minute}`).then(video => {
videoPlayStream.videoObject[minute] = video;
return video;
});
}
},
// 跳到指定的時間
jumpTo: minute => {
// 如果影片片段影存在,直接播放
if (videoPlayStream.videoObject[minute]) {
videoPlayStream.play(minute);
} else {
// 如果影片片段不存在,先進行下載,然後再播放
videoPlayStream.downloadVideo(minute).then(video => {
videoPlayStream.play(minute);
});
}
},
// 播放指定時間影片片段
play: minute => {
// 實際播放影片的邏輯
// 同時預先下載下一個時間點的片段
videoPlayStream.downloadVideo(minute + 1);
}
};
// 從頭開始播放
videoPlayStream.jumpTo(0);
// 跳到第 45 分鐘播放
videoPlayStream.jumpTo(45);
當然以上程式碼還有很大的優化空間,也有很多未處理的細節,實際上也絕對跑不動,但作為範例,大概就會像這樣的概念去處理串流。
在 ReactiveX 的觀念中,我們會將所有發生的事情都視為串流!
以網頁為例子,滑鼠的點擊事件可以視為一連串事件的串流,除非網頁關閉,否則這個事件持續可能會發生。
當 HTTP 請求呼叫時(也就是 AJAX),也是一種串流,只是這種串流事件只發生一次就會結束
既然所有行為都可以視為串流,如何整合這些串流就變得非常重要,若沒有妥善的設計,很容易就會發生串流包串流這種巢狀地獄的發生,任何有經驗的開發人員應該想盡辦法避免這種狀況!而 ReactiveX 就是結合了多種程式設計的觀念和技巧,漂亮的解決了這些問題!關於這些觀念和技巧,我們之後再來說明。現在我們只需要先有一個觀念,就是盡量以串流的方式思考就好,未來我們會學習到各種組合串流的方式。
今天我們介紹了「非同步」與「串流」的基本觀念與問題。
非同步的主要目標是不要為了等待而造成後續程式無法進行的問題,但非同步還是一段執行完就結束的程式碼,因此比較無法處理連續性的資料。
這時候就要搭配串流的概念來設計,而比起非同步處理完就結束,串流相對比較難以掌控及預測,在開發上需要更多的技巧來輔助以避免程式太過複雜難以維護。
ReactiveX 的出現就是為了解決的這個問提,在接下來幾天的文章,我們將一一介紹 ReactiveX 背後組合的各種觀念和程式技巧,掌握這些技巧後,就能更加靈活的組合出強固的串流處理邏輯囉!